package com.authsphere.security.authorization;

import com.authsphere.security.token.common.AccessToken;
import com.authsphere.security.token.common.AuthenticationTokenServices;
import com.authsphere.security.token.common.codec.DefaultBase64Codec;
import com.authsphere.security.token.common.codec.TokenCodec;
import com.authsphere.security.token.common.exception.AccessTokenAuthenticationException;
import com.authsphere.security.token.common.exception.InvalidTokenExceptionException;
import com.authsphere.security.token.common.exception.TokenTheftException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.NullRememberMeServices;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.CookieTheftException;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;

import java.io.IOException;
import java.util.function.Supplier;

/**
 * @program: AuthSphere
 * @description:
 * @author: YuKai Fan
 * @create: 2025/3/13 10:08
 **/
public class AuthorizationTokenFilter extends AbstractAuthenticationProcessingFilter {

    private final static String AUTHSPHERE_AUTHORIZATION_URL = "/authorization";

    private String tokenHeader = HttpHeaders.AUTHORIZATION;
    private String parameterName = HttpHeaders.AUTHORIZATION;

    private TokenCodec tokenCodec = new DefaultBase64Codec();

    private RememberMeServices rememberMeServices;

    private AuthenticationTokenServices authenticationTokenServices;

    private Supplier<AuthorizationRequest> supplierAuthorizationRequest = () -> new AuthorizationRequest() {
        @Override
        public boolean needAutoRenewal() {
            return true;
        }

        @Override
        public int renewalInterval() {
            return 300;
        }
    };

    public AuthorizationTokenFilter() {
        super(AUTHSPHERE_AUTHORIZATION_URL);
    }

    public AuthorizationTokenFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    public AuthorizationTokenFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        super(requiresAuthenticationRequestMatcher);
    }

    public AuthorizationTokenFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(defaultFilterProcessesUrl, authenticationManager);
    }

    public AuthorizationTokenFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
        super(requiresAuthenticationRequestMatcher, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (rememberMeServices != null && !(rememberMeServices instanceof NullRememberMeServices)) {
            return rememberMeServices.autoLogin(request, response);
        }
        String value = getAuthorizationValue(request);
        if (StringUtils.isBlank(value)) {
            throw new AccessTokenAuthenticationException("Login Invalid");
        }
        String[] decodeTokens = tokenCodec.decode(value);
        return processAutoLogin(decodeTokens, request, response);
    }

    private boolean hasAuthorizationToken(HttpServletRequest request) {
        String value = getAuthorizationValue(request);
        return StringUtils.isNotBlank(value);
    }

    private String getAuthorizationValue(HttpServletRequest request) {
        return StringUtils.defaultIfBlank(request.getHeader(tokenHeader), request.getParameter(parameterName));
    }

    public String getTokenHeader() {
        return tokenHeader;
    }

    public void setTokenHeader(String tokenHeader) {
        this.tokenHeader = tokenHeader;
    }

    public void setParameterName(String parameterName) {
        this.parameterName = parameterName;
    }

    public Supplier<AuthorizationRequest> getSupplierAuthorizationRequest() {
        return supplierAuthorizationRequest;
    }

    public void setSupplierAuthorizationRequest(Supplier<AuthorizationRequest> supplierAuthorizationRequest) {
        Assert.notNull(supplierAuthorizationRequest, "supplierAuthorizationRequest cannot be null");
        this.supplierAuthorizationRequest = supplierAuthorizationRequest;
    }

    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
        this.rememberMeServices = rememberMeServices;
    }

    private Authentication processAutoLogin(String[] tokens, HttpServletRequest request,
                                              HttpServletResponse response) {
        if (tokens.length != 2) {
            throw new InvalidTokenExceptionException(" token encodes error");
        }
        String presentedSeries = tokens[0];
        String presentedToken = tokens[1];
        AccessToken token = this.authenticationTokenServices.getAccessToken(presentedSeries);
        if (token == null) {
            // No series match, so we can't authenticate using this cookie
            throw new AccessTokenAuthenticationException("No persistent token found for series ");
        }
        // We have a match for this user/series combination
        if (!presentedToken.equals(token.getValue())) {
            // Token doesn't match series value. Delete all logins for this user and throw
            // an exception to warn them.
            this.authenticationTokenServices.removeAccessToken(token.getSeriesId());
            throw new TokenTheftException(this.messages.getMessage(
                    "AuthorizationTokenFilter.processAutoLogin",
                    "Invalid token mismatch. Implies previous token theft attack."));
        }
        if (token.isExpired()) {
            throw new AccessTokenAuthenticationException(" login has expired");
        }
        final AuthorizationAuthenticationToken authorizationAuthenticationToken = new AuthorizationAuthenticationToken(token, supplierAuthorizationRequest.get());
        return this.getAuthenticationManager().authenticate(authorizationAuthenticationToken);
    }

    public void setAuthenticationTokenServices(AuthenticationTokenServices authenticationTokenServices) {
        this.authenticationTokenServices = authenticationTokenServices;
    }

    public void setTokenCodec(TokenCodec tokenCodec) {
        this.tokenCodec = tokenCodec;
    }
}
